﻿using System;
using System.Linq;
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Sencha.Serialization.Contracts
{
	internal sealed class DataContractInterpreter : MetadataInterpreter
	{
		private static readonly Type[] ConstructorWithTypeParameter = new Type[] { typeof(Type) };

		private enum TypeKind : byte
		{
			WithContract, // explicit include
			WithoutContract, // explicit exclude
			WithoutContractSerializable // include/exclude/names ignored
		}

		public override TypeDescription Interpret(DataContract contract, MetadataDescriber descriptor, Type type)
		{
			if (contract == null)
				throw new ArgumentNullException("contract");
			if (descriptor == null)
				throw new ArgumentNullException("descriptor");
			if (type == null)
				throw new ArgumentNullException("type");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			TypeDescription typeInfo;

			if (contract.TryGetKnownType(type, out typeInfo))
				return typeInfo;

			var unwrappedType = type;
			if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
				unwrappedType = Nullable.GetUnderlyingType(type);
			else if (type.IsPointer || type.IsByRef)
				unwrappedType = type.GetElementType();
			var typeKind = this.GetTypeKind(unwrappedType);

			var members = new List<MemberDescription>(20);

			// constructing new typeinfo
			typeInfo = new TypeDescription(type);
			typeInfo.Members = members;
			typeInfo.Attributes = descriptor.GetAttributes(type);
			typeInfo.IsBuiltIn = false;
			typeInfo.IsArray = type.IsArray;
			typeInfo.IsPrimitive = unwrappedType.IsPrimitive;
			typeInfo.IsReferenceType = !type.IsValueType;
			typeInfo.IsValueType = unwrappedType.IsValueType || unwrappedType.IsEnum;
			typeInfo.IsInterface = type.IsInterface;
			typeInfo.IsSerializable = unwrappedType.IsSerializable || typeInfo.Attributes.Any(a => a is SerializableAttribute);
			typeInfo.IsJsonSerializable = typeof(IJsonSerializable).IsAssignableFrom(type);
			typeInfo.IsNullable = typeInfo.IsReferenceType || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>));
			typeInfo.IsObjectType = type == typeof(object);
			typeInfo.IsDictionary = typeof(System.Collections.IDictionary).IsAssignableFrom(type) ||
					type.IsInstantiationOf(typeof(IDictionary<,>));
			typeInfo.IsEnumerable = (typeInfo.IsArray ||
				typeof(System.Collections.IEnumerable).IsAssignableFrom(type) ||
				type.IsInstantiationOf(typeof(IEnumerable<>))) && type != typeof(string) && !typeInfo.IsDictionary;
			typeInfo.IsNumeric = Array.IndexOf(TypeDescription.Numbers, unwrappedType) != -1;
			typeInfo.IsAnonymousType = type.IsSealed && type.IsNotPublic && type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), true) != null;
			typeInfo.IsPrivate = type.IsNotPublic;

			base.AddTypeInfoToContract(contract, typeInfo);

			if (typeInfo.IsDictionary)
				typeInfo.UnderlyingType = this.Interpret(contract, descriptor, type.IsInstantiationOf(typeof(IDictionary<,>)) ? type.GetInstantiationArguments(typeof(IDictionary<,>))[1] : typeof(object));
			else if (typeInfo.IsEnumerable)
				typeInfo.UnderlyingType = this.Interpret(contract, descriptor, type.IsInstantiationOf(typeof(IEnumerable<>)) ? type.GetInstantiationArguments(typeof(IEnumerable<>))[0] : typeof(object));
			else if (type.IsEnum)
				typeInfo.UnderlyingType = this.Interpret(contract, descriptor, Enum.GetUnderlyingType(type));
			else if (typeInfo.IsNullable && typeInfo.IsValueType)
				typeInfo.UnderlyingType = this.Interpret(contract, descriptor, type.GetGenericArguments()[0]);

			if (type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof(TypeSerializer<>))
			{
				var genericArgs = type.GetInstantiationArguments(typeof(TypeSerializer<>));
				var serializedType = this.Interpret(contract, descriptor, genericArgs[0]);

				typeInfo.IsTypeSerializer = true;
				typeInfo.SerializerOptions = this.GetTypSerializerOptions(typeInfo.Attributes);

				var surrogatedBy = contract.AddTypeSerializer(serializedType, typeInfo);
				if (surrogatedBy != typeInfo)
					throw new Exceptions.TypeIsAlreadyHasSerializer(type, serializedType.GetClrType(), surrogatedBy.GetClrType());
			}

			var serializerForType = this.GetTypeSerializer(typeInfo.Attributes);
			if (serializerForType != null)
				this.Interpret(contract, descriptor, serializerForType);

			if ((type.IsInterface && type.BaseType == null) || typeInfo.IsTypeSerializer)
			{
				typeInfo.BaseType = contract.GetKnownType(typeof(object));
				contract.AddTypeHeir(typeInfo.BaseType, typeInfo);
			}
			else if (type.BaseType != null)
			{
				typeInfo.BaseType = this.Interpret(contract, descriptor, type.BaseType);
				contract.AddTypeHeir(typeInfo.BaseType, typeInfo);
			}

			typeInfo.Name = this.GetMemberName(descriptor, typeKind, type);
			typeInfo.TypeFullName = type.FullName;

			if (!typeInfo.IsTypeSerializer)
			{
				typeInfo.IsConvertible = this.HasConverterType(typeInfo.Attributes) || typeof(IConvertible).IsAssignableFrom(type);

				if (!typeInfo.IsPrimitive && !typeInfo.IsEnumerable && !typeInfo.IsDictionary)
				{
					// discovering members
					foreach (var member in descriptor.GetMembers(type))
					{
						if (!IsMemberIncluded(descriptor, type, member))
							continue;

						var declaringTypeInfo = typeInfo;
						var declaringType = descriptor.GetDeclaringType(member);
						if (declaringType != type)
							declaringTypeInfo = contract.GetKnownType(declaringType);

						var memberInfo = new MemberDescription(declaringTypeInfo, member);
						memberInfo.Name = this.GetMemberName(descriptor, typeKind, member);
						memberInfo.NameAsJson = JsonUtils.EscapeAndQuote(memberInfo.Name) + ":";
						memberInfo.FieldName = string.Intern(descriptor.GetName(member));
						//memberInfo.MemberType = descriptor.GetMemberType(member);
						//memberInfo.MemberTypeInfo = this.Interpret(contract, descriptor, memberInfo.MemberType);
						//memberInfo.IsReadOnly = descriptor.IsMemberReadOnly(member);
						memberInfo.Attributes = descriptor.GetAttributes(member);
						//memberInfo.ConverterType = this.GetConverterType(memberInfo.Attributes);
						//memberInfo.AlternativeMemberTypes = (IList<TypeInfo>)Enumerable.Empty<TypeInfo>();
						memberInfo.Ordinal = typeInfo.Members.Count;

						members.Add(memberInfo);
					}
					members.Sort(this.CompareMembers);
				}

			}
			typeInfo.Members = members.AsReadOnly();

			// finding references to other types
			this.FindReferencedTypes(contract, typeInfo);

			return typeInfo;
		}

		private void FindReferencedTypes(DataContract contract, TypeDescription typeInfo)
		{
			if (contract == null)
				throw new ArgumentNullException("contract");
			if (typeInfo == null)
				throw new ArgumentNullException("typeInfo");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			List<Type> referencedTypes = new List<Type>(100);

			foreach (var memberInfo in typeInfo.Members)
			{
				foreach (var attr in memberInfo.Attributes)
				{
					if (attr is KnownTypeAttribute)
					{
						var kta = attr as KnownTypeAttribute;
						referencedTypes.Add(kta.Type);
					}
				}
			}

			foreach (var attr in typeInfo.Attributes)
			{
				if (attr is KnownTypeAttribute)
				{
					var kta = attr as KnownTypeAttribute;
					referencedTypes.Add(kta.Type);
				}
			}

			var type = typeInfo.GetClrType();
			// special cases of generic types
			if (type.HasElementType) // array, typeRef
				referencedTypes.Add(type.GetElementType());
			if (typeInfo.IsDictionary && type.IsInstantiationOf(typeof(IDictionary<,>))) // dictionary
				referencedTypes.AddRange(type.GetInstantiationArguments(typeof(IDictionary<,>)));
			else if (typeInfo.IsEnumerable && !typeInfo.IsDictionary && type.IsInstantiationOf(typeof(IEnumerable<>))) // IEnumerable, ICollection, IList
				referencedTypes.AddRange(type.GetInstantiationArguments(typeof(IEnumerable<>)));

			referencedTypes.AddRange(type.GetInterfaces());

			for (int i = 0; i < referencedTypes.Count; i++)
				contract.AddKnownType(referencedTypes[i]);
		}
		private bool HasConverterType(IList<Attribute> attributes)
		{
			if (attributes == null)
				throw new ArgumentNullException("attributes");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			foreach (Attribute attr in attributes)
			{
				if (attr is TypeConverterAttribute)
				{
					var converterAttr = attr as TypeConverterAttribute;
					return true;
				}
			}
			return false;
		}
		private Type GetConverterType(IList<Attribute> attributes)
		{
			if (attributes == null)
				throw new ArgumentNullException("attributes");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			foreach (Attribute attr in attributes)
			{
				if (attr is TypeConverterAttribute)
				{
					var converterAttr = attr as TypeConverterAttribute;
					return Type.GetType(converterAttr.ConverterTypeName, true);
				}
			}
			return null;
		}
        private TypeKind GetTypeKind(Type type)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            System.Diagnostics.Contracts.Contract.EndContractBlock();

            var contract = Attribute.IsDefined(type, typeof(DataContractAttribute), false) || Attribute.IsDefined(type, typeof(CollectionDataContractAttribute), false);
            var serializable = Attribute.IsDefined(type, typeof(SerializableAttribute), false);
            var xmlserializable = typeof(System.Xml.Serialization.IXmlSerializable).IsAssignableFrom(type);

            if (contract)
                return TypeKind.WithContract;
            else if (!contract && !serializable && !xmlserializable)
                return TypeKind.WithoutContract;
            else
                return TypeKind.WithoutContractSerializable;
        }
		private TypeSerializerOptions GetTypSerializerOptions(IList<Attribute> attributes)
		{
			if (attributes == null)
				throw new ArgumentNullException("attributes");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			foreach (var attr in attributes)
			{
				if (attr is TypeSerializerAttribute)
					return (attr as TypeSerializerAttribute).Options;
			}
			return TypeSerializerOptions.None;
		}
		private Type GetTypeSerializer(IList<Attribute> attributes)
		{
			if (attributes == null)
				throw new ArgumentNullException("attributes");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			foreach (var attr in attributes)
			{
				if (attr is TypeSerializerAttribute)
					return (attr as TypeSerializerAttribute).SerializerType;
			}
			return null;
		}
        private string GetMemberName(MetadataDescriber descriptor, TypeKind typeKind, System.Reflection.MemberInfo member)
		{
			if (descriptor == null)
				throw new ArgumentNullException("descriptor");
			if (member == null)
				throw new ArgumentNullException("member");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var name = descriptor.GetName(member);

			foreach (Attribute attr in descriptor.GetAttributes(member))
			{
				if (attr is DataContractAttribute)
				{
					var dataContractAttr = attr as DataContractAttribute;
					var nameRedefine = dataContractAttr.Name;
					if (!string.IsNullOrEmpty(nameRedefine))
						return string.Intern(nameRedefine);
				}
				else if (attr is DataMemberAttribute && typeKind == TypeKind.WithContract)
				{
					var dataMemberAttr = attr as DataMemberAttribute;
					var nameRedefine = dataMemberAttr.Name;
					if (!string.IsNullOrEmpty(nameRedefine))
						return string.Intern(nameRedefine);
				}
			}

			return name;
		}
		private bool IsMemberIncluded(MetadataDescriber descriptor, Type type, System.Reflection.MemberInfo member)
		{
			if (descriptor == null)
				throw new ArgumentNullException("descriptor");
			if (type == null)
				throw new ArgumentNullException("type");
			if (member == null)
				throw new ArgumentNullException("member");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var declaringType = descriptor.GetDeclaringType(member);
			var declaringTypeKind = this.GetTypeKind(declaringType);
			var attrs = descriptor.GetAttributes(member);

			if (declaringTypeKind == TypeKind.WithContract)
				return DataContractInterpreter.HasAttribute<DataMemberAttribute>(attrs);
			else if (declaringTypeKind == TypeKind.WithoutContract)
				return !DataContractInterpreter.HasAttribute<IgnoreDataMemberAttribute>(attrs) && (descriptor.GetMemberAccessModifiers(member) & MemberAccessModifiers.PublicGet) != 0;
			else
				return (descriptor.GetMemberAccessModifiers(member) & MemberAccessModifiers.PublicGet) != 0;
		}
		private int CompareMembers(MemberDescription x, MemberDescription y)
		{
			if (x == null && y == null)
				return 0;
			else if (x == null)
				return -1;
			else if (y == null)
				return 1;

			var xMemberAttribute = DataContractInterpreter.FindAttribute<DataMemberAttribute>(x.Attributes);
			var yMemberAttribute = DataContractInterpreter.FindAttribute<DataMemberAttribute>(y.Attributes);

			var xDepth = DataContractInterpreter.GetMemberDepth(x.DeclaringType);
			var yDepth = DataContractInterpreter.GetMemberDepth(y.DeclaringType);
			var xOrder = xMemberAttribute != null ? xMemberAttribute.Order * 2 : x.Ordinal * 2 - 1;
			var yOrder = yMemberAttribute != null ? yMemberAttribute.Order * 2 : y.Ordinal * 2 - 1;
			var xName = x.Name;
			var yName = y.Name;

			if (xDepth == yDepth) // by depth
			{
				if (xOrder == yOrder) // then by order
					return xName.CompareTo(yName); // then by name 
				else
					return xOrder.CompareTo(yOrder);
			}
			else
				return xDepth.CompareTo(yDepth);
		}

		private static bool HasAttribute<AttrT>(object[] attributes) where AttrT : Attribute
		{
			if (attributes == null)
				throw new ArgumentNullException("attributes");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			for (int i = 0; i < attributes.Length; i++)
			{
				if (attributes[i] is AttrT)
					return true;
			}
			return false;
		}
		private static AttrT FindAttribute<AttrT>(System.Collections.Generic.ICollection<Attribute> attributes) where AttrT : Attribute
		{
			if (attributes == null)
				throw new ArgumentNullException("attributes");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			foreach (var attr in attributes)
			{
				if (attr is AttrT)
					return (AttrT)attr;
			}
			return null;
		}
		private static int GetMemberDepth(TypeDescription typeInfo)
		{
			if (typeInfo == null)
				throw new ArgumentNullException("typeInfo");

			var depth = 0;
			var baseType = typeInfo;
			while (baseType != null)
			{
				depth++;
				baseType = baseType.BaseType;
			}
			return depth;
		}
	}
}
